/*
  Copyright 2012 Sergey Ostanin

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/

package ru.irenproject.it;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.inject.Inject;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import org.apache.commons.lang3.Validate;

import ru.irenproject.MarkScale;
import ru.irenproject.Modifier;
import ru.irenproject.ModifierList;
import ru.irenproject.Profile;
import ru.irenproject.ProfileList;
import ru.irenproject.Question;
import ru.irenproject.QuestionItem;
import ru.irenproject.QuestionList;
import ru.irenproject.Section;
import ru.irenproject.SectionTree;
import ru.irenproject.Test;
import ru.irenproject.TestProtos.AvailableQuestionScore;
import ru.irenproject.TestProtos.AvailableScore;
import ru.irenproject.TestProtos.AvailableSectionScore;
import ru.irenproject.TestProtos.SectionNode;
import ru.irenproject.pad.Image;
import ru.irenproject.pad.ImageSource;
import ru.irenproject.pad.Pad;
import ru.irenproject.pad.PadProtos.Block;
import ru.irenproject.pad.PadProtos.ImageFormat;

import com.google.common.math.IntMath;
import com.google.inject.assistedinject.Assisted;
import com.google.protobuf.ByteString;

public final class ItWriter {
  private static final long ZIP_ENTRY_TIME = 0;

  static ZipEntry createZipEntryWithFixedTime(String name) {
    ZipEntry res = new ZipEntry(name);
    res.setTime(ZIP_ENTRY_TIME);
    return res;
  }

  private final Map<Class<? extends Question>, QuestionWriter> fQuestionWriters;
  private final Map<Class<? extends Modifier>, ModifierWriter> fModifierWriters;
  private final XMLStreamWriter fOut;
  private final ZipOutputStream fZip;
  private final Map<ByteString, Integer> fImages = new HashMap<>();
  private boolean fSkipImageSource;

  @Inject ItWriter(
      Map<Class<? extends Question>, QuestionWriter> questionWriters,
      Map<Class<? extends Modifier>, ModifierWriter> modifierWriters,
      @Assisted XMLStreamWriter out,
      @Assisted ZipOutputStream zip) {
    fQuestionWriters = questionWriters;
    fModifierWriters = modifierWriters;
    fOut = out;
    fZip = zip;
  }

  public void begin(String elementName) {
    try {
      fOut.writeStartElement(elementName);
    } catch (XMLStreamException e) {
      throw new RuntimeException(e);
    }
  }

  public void end() {
    try {
      fOut.writeEndElement();
    } catch (XMLStreamException e) {
      throw new RuntimeException(e);
    }
  }

  public void add(String attributeName, String attributeValue) {
    try {
      fOut.writeAttribute(attributeName, attributeValue);
    } catch (XMLStreamException e) {
      throw new RuntimeException(e);
    }
  }

  public void addBoolean(String attributeName, boolean value) {
    add(attributeName, Boolean.toString(value));
  }

  public void writeQuestionItem(QuestionItem v) {
    Question q = v.question();
    QuestionWriter qw = Validate.notNull(fQuestionWriters.get(q.getClass()));
    begin(It.ELEM_QUESTION);
    add(It.ATTR_TYPE, qw.type());
    add(It.ATTR_WEIGHT, Integer.toString(v.weight()));
    addBoolean(It.ATTR_ENABLED, v.enabled());
    qw.write(q, this);

    List<String> labels = v.labelList();
    if (!labels.isEmpty()) {
      begin(It.ELEM_LABELS);
      for (String label : labels) {
        begin(It.ELEM_LABEL);
        add(It.ATTR_VALUE, label);
        end();
      }
      end();
    }

    writeModifierList(v.modifierList());
    end();
  }

  public void writePad(Pad pad) {
    try {
      begin(It.ELEM_CONTENT);
      for (Block b : pad.blockList()) {
        switch (b.getType()) {
          case TEXT: {
            begin(It.ELEM_TEXT);
            add(It.ATTR_VALUE, b.getTextBlock().getText());
            end();
            break;
          }
          case IMAGE: {
            ByteString imageId = b.getImageBlock().getImage();
            Integer imageNumber = fImages.get(imageId);
            boolean newImage = (imageNumber == null);
            if (newImage) {
              imageNumber = IntMath.checkedAdd(fImages.size(), 1);
              fImages.put(imageId, imageNumber);
            }

            Image image = pad.store().get(imageId, Image.class);
            String imageFileName = String.format("%s/%d.%s", It.IT_IMAGE_DIRECTORY_NAME,
                imageNumber, getImageExtension(image.format()));

            if (newImage) {
              byte[] data = image.data().toByteArray();
              ZipEntry entry = createZipEntryWithFixedTime(imageFileName);
              entry.setMethod(ZipEntry.STORED);
              entry.setSize(data.length);
              entry.setCrc(getCrc32(data));

              fZip.putNextEntry(entry);
              fZip.write(data);
              fZip.closeEntry();
            }

            begin(It.ELEM_IMG);
            add(It.ATTR_SRC, imageFileName);

            if (image.hasSource() && !fSkipImageSource) {
              ImageSource source = image.source();
              String sourceFileName = String.format("%s/%d.%s", It.IT_IMAGE_DIRECTORY_NAME,
                  imageNumber, It.IT_SOURCE_DATA_FILE_EXTENSION);

              if (newImage) {
                fZip.putNextEntry(createZipEntryWithFixedTime(sourceFileName));
                fZip.write(source.data().toByteArray());
                fZip.closeEntry();
              }

              add(It.ATTR_CLIPBOARD_FORMAT, source.format());
              add(It.ATTR_CLIPBOARD_DATA, sourceFileName);
            }

            end();
            break;
          }
          case LINE_FEED: {
            begin(It.ELEM_BR);
            end();
            break;
          }
        }
      }
      end();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  private static String getImageExtension(ImageFormat f) {
    switch (f) {
      case JPEG: return It.IT_IMAGE_JPG_EXTENSION;
      case PNG: return It.IT_IMAGE_PNG_EXTENSION;
    }
    throw new RuntimeException();
  }

  private static long getCrc32(byte[] data) {
    CRC32 c = new CRC32();
    c.update(data);
    return c.getValue();
  }

  public void writeModifierList(ModifierList list) {
    if (!list.isEmpty()) {
      begin(It.ELEM_MODIFIERS);
      for (Modifier m : list.list()) {
        begin(It.ELEM_MODIFIER);
        ModifierWriter writer = Validate.notNull(fModifierWriters.get(m.getClass()));
        add(It.ATTR_TYPE, writer.type());
        writer.write(m, this);
        end();
      }
      end();
    }
  }

  public void writeProfile(Profile p) {
    begin(It.ELEM_PROFILE);
    add(It.ATTR_TITLE, p.title());

    begin(It.ELEM_QUESTION_SELECTION);
    add(It.ATTR_QUESTIONS_PER_SECTION, p.hasQuestionsPerSection() ?
        Integer.toString(p.questionsPerSection()) : It.VAL_ALL);
    if (p.hasLabelFilter()) {
      add(It.ATTR_LABEL_FILTER, p.labelFilter());
    }
    addBoolean(It.ATTR_SHUFFLE_QUESTIONS, p.shuffleQuestions());
    end();

    begin(It.ELEM_SESSION_OPTIONS);
    add(It.ATTR_DURATION_MINUTES, p.hasDurationMinutes() ?
        Integer.toString(p.durationMinutes()) : It.VAL_UNLIMITED);
    addBoolean(It.ATTR_EDITABLE_ANSWERS, p.editableAnswers());
    addBoolean(It.ATTR_BROWSABLE_QUESTIONS, p.browsableQuestions());
    addBoolean(It.ATTR_WEIGHT_CUES, p.weightCues());
    end();

    if (!p.editableAnswers()) {
      begin(It.ELEM_INSTANT_FEEDBACK);
      addBoolean(It.ATTR_ANSWER_CORRECTNESS, p.instantAnswerCorrectness());
      addBoolean(It.ATTR_TOTAL_PERCENT_CORRECT, p.instantTotalPercentCorrect());
      end();
    }

    if (p.hasAvailableScore()) {
      writeAvailableScore(p.availableScore());
    }

    writeMarkScale(p.markScale());
    writeModifierList(p.modifierList());

    end();
  }

  private void writeAvailableScore(AvailableScore s) {
    begin(It.ELEM_TEST_RESULTS);
    addBoolean(It.ATTR_PERCENT_CORRECT, s.getPercentCorrect());
    addBoolean(It.ATTR_POINTS, s.getPoints());
    addBoolean(It.ATTR_MARK, s.getMark());

    if (s.hasForQuestions()) {
      writeAvailableQuestionScore(s.getForQuestions());
    }

    if (s.hasForSections()) {
      writeAvailableSectionScore(s.getForSections());
    }

    end();
  }

  private void writeAvailableQuestionScore(AvailableQuestionScore s) {
    begin(It.ELEM_QUESTION_RESULTS);
    addBoolean(It.ATTR_PERCENT_CORRECT, s.getPercentCorrect());
    addBoolean(It.ATTR_POINTS, s.getPoints());
    addBoolean(It.ATTR_CORRECT_ANSWER, s.getCorrectAnswer());
    end();
  }

  private void writeAvailableSectionScore(AvailableSectionScore s) {
    begin(It.ELEM_SECTION_RESULTS);
    addBoolean(It.ATTR_PERCENT_CORRECT, s.getPercentCorrect());
    addBoolean(It.ATTR_POINTS, s.getPoints());
    addBoolean(It.ATTR_QUESTION_COUNT, s.getQuestionCount());
    addBoolean(It.ATTR_QUESTION_LIST, s.getQuestionList());
    end();
  }

  private void writeMarkScale(MarkScale s) {
    if (!s.isEmpty()) {
      begin(It.ELEM_MARK_SCALE);
      begin(It.ELEM_MARKS);
      int n = s.count();
      for (int i = 0; i < n; ++i) {
        begin(It.ELEM_MARK);
        add(It.ATTR_TITLE, s.getTitle(i));
        add(It.ATTR_LOWER_BOUND, String.format(Locale.ENGLISH, "%.4f", s.getLowerBound(i)));
        end();
      }
      end();
      end();
    }
  }

  public void writeTest(Test test) {
    writeSectionTree(test.sectionTree());
    writeProfileList(test.profileList());
  }

  public void writeSectionTree(SectionTree sectionTree) {
    writeSection(sectionTree.rootNode(), sectionTree);
  }

  private void writeSection(SectionNode node, SectionTree sectionTree) {
    Section section = sectionTree.store().get(node.getSection(), Section.class);
    begin(It.ELEM_SECTION);
    add(It.ATTR_TITLE, section.name());
    writeQuestionList(section.questionList());

    if (node.getChildCount() > 0) {
      begin(It.ELEM_SECTIONS);
      for (SectionNode child : node.getChildList()) {
        writeSection(child, sectionTree);
      }
      end();
    }

    writeModifierList(section.modifierList());
    end();
  }

  private void writeQuestionList(QuestionList list) {
    if (!list.isEmpty()) {
      begin(It.ELEM_QUESTIONS);
      for (QuestionItem item : list) {
        writeQuestionItem(item);
      }
      end();
    }
  }

  private void writeProfileList(ProfileList list) {
    writeProfiles(list.list());
  }

  public void writeProfiles(Collection<Profile> profiles) {
    if (!profiles.isEmpty()) {
      begin(It.ELEM_PROFILES);
      for (Profile profile : profiles) {
        writeProfile(profile);
      }
      end();
    }
  }

  public void setSkipImageSource() {
    fSkipImageSource = true;
  }
}
